package org.xx.armory.db.impl;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.commons.AbstractLifeCycle;
import org.xx.armory.db.ConnectionRef;
import org.xx.armory.db.DbException;
import org.xx.armory.db.DbRow;
import org.xx.armory.db.Mapper;
import org.xx.armory.db.Parameter;
import org.xx.armory.db.ParameterBuilder;
import org.xx.armory.db.ParameterDirection;
import org.xx.armory.db.ParameterType;
import org.xx.armory.db.Session;
import org.xx.armory.db.Transaction;

import java.io.ByteArrayInputStream;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.sql.Types.BIGINT;
import static java.sql.Types.BIT;
import static java.sql.Types.BLOB;
import static java.sql.Types.BOOLEAN;
import static java.sql.Types.CHAR;
import static java.sql.Types.CLOB;
import static java.sql.Types.DATE;
import static java.sql.Types.DECIMAL;
import static java.sql.Types.DOUBLE;
import static java.sql.Types.FLOAT;
import static java.sql.Types.INTEGER;
import static java.sql.Types.LONGVARBINARY;
import static java.sql.Types.REAL;
import static java.sql.Types.REF_CURSOR;
import static java.sql.Types.TIMESTAMP;
import static java.sql.Types.TINYINT;
import static java.sql.Types.VARBINARY;
import static java.sql.Types.VARCHAR;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.repeat;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.xx.armory.commons.SysUtils.readString;
import static org.xx.armory.commons.Validators.greaterThanOrEqual;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notNull;
import static org.xx.armory.commons.Validators.validIndex;

/**
 * 提供了会话的基本功能。
 * <p>该类型的会话必须关联一个事务。</p>
 *
 * @author Haart
 * @see Transaction
 */
abstract class AbstractSession
        extends AbstractLifeCycle
        implements Session {
    /**
     * 用于匹配动态参数的正则表达式。
     * <p>
     * 该正则表达式可以匹配两种参数：{@literal #...#}或者{@literal $...$}。前者表示定位参数，后者表示文本替换参数。</p>
     */
    private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile(
            "(#([a-zA-Z_][a-zA-Z0-9_]*)#)|(\\$([a-zA-Z_][a-zA-Z0-9_]*)\\$)",
            Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
    private final Logger logger = LoggerFactory.getLogger(AbstractSession.class);
    private final Transaction transaction;
    private final List<Parameter> parameters;
    private ConnectionRef connection;
    private Object lastRowId;
    private int startRowIndex;
    private int maximumRows;
    private int fetchSize;
    private String autoKey;

    /**
     * 构造{@code AbstractSession}实例。
     *
     * @param transaction
     *         当前会话关联的事务。
     * @throws IllegalArgumentException
     *         如果参数{@code transaction}是{@code null}。
     */
    AbstractSession(
            Transaction transaction
    ) {
        this.transaction = notNull(transaction, "transaction");
        this.connection = null;
        this.autoKey = "";
        this.lastRowId = null;
        this.parameters = new ArrayList<>();
        this.startRowIndex = 0;
        this.maximumRows = 0;
        this.fetchSize = 0;
    }

    /**
     * 根据名称获取指定的参数。
     *
     * @param parameterName
     *         参数名称。
     * @param parameters
     *         待查找的参数集合。
     * @return 符合条件的参数。如果参数{@code parameters}是{@code null}或者不包含任何元素，或者找不到符合条件的参数则返回{@code null}。
     * @throws NullPointerException
     *         如果参数{@code parameterName}是{@code null}。
     * @throws IllegalArgumentException
     *         如果参数{@code name}只包含空白字符。
     */
    private static Parameter getParameterByName(
            String parameterName,
            Collection<? extends Parameter> parameters
    ) {
        notBlank(parameterName, "parameterName");

        if (parameters == null || parameters.size() == 0) {
            return null;
        }

        for (final Parameter parameter : parameters) {
            if (parameter == null) {
                continue;
            }

            if (parameterName.equalsIgnoreCase(parameter.getName())) {
                return parameter;
            }
        }

        return null;
    }

    /**
     * 获取返回参数。
     *
     * @param statement
     *         被执行的{@link CallableStatement}。
     * @param parameters
     *         所有参数。
     * @throws SQLException
     *         获取返回参数的值时出错。
     */
    private static void retrieveOutParameters(
            CallableStatement statement,
            List<? extends Parameter> parameters
    )
            throws SQLException {
        notNull(statement, "statement");
        notNull(parameters, "parameters");

        for (int i = 0; i < parameters.size(); ++i) {
            final Parameter parameter = parameters.get(i);
            if (parameter.getDirections().contains(ParameterDirection.OUT)) {
                switch (parameter.getParameterType()) {
                    case STRING:
                        parameter.setValue(statement.getString(i + 1));
                        break;
                    case INTEGER:
                        final BigDecimal iv = statement.getBigDecimal(i + 1);
                        parameter.setValue(iv == null ? null : iv.longValue());
                        break;
                    case DECIMAL:
                        parameter.setValue(statement.getBigDecimal(i + 1));
                        break;
                    case DATETIME:
                    case START_DATE:
                    case START_MONTH:
                    case START_YEAR:
                    case END_DATE:
                    case END_MONTH:
                    case END_YEAR:
                        parameter.setValue(statement.getTimestamp(i + 1));
                    case DOUBLE:
                        final BigDecimal decimalValue = statement.getBigDecimal(i + 1);
                        parameter.setValue(decimalValue == null ? null : decimalValue.doubleValue());
                        break;
                    case BOOLEAN:
                        final BigDecimal booleanValue = statement.getBigDecimal(i + 1);
                        parameter.setValue(booleanValue == null ? null : booleanValue.intValue() != 0);
                        break;
                    case BINARY:
                        final java.sql.Blob blobValue = statement.getBlob(i + 1);
                        if (blobValue != null) {
                            try {
                                parameter.setValue(blobValue.getBytes(1, (int) blobValue.length()));
                            } finally {
                                blobValue.free();
                            }
                        }
                        break;
                    case RESULT_SET:
                        // 包含结果集的返回参数，不取回值，此结果集通过 getProcResultSet 方法获取。
                        parameter.setValue(null);
                        break;
                    default:
                        throw new IllegalArgumentException("illegal parameter type: " + parameter.getParameterType());
                }
            }
        }
    }

    /**
     * 将结果集中的一行映射为对象数组。
     *
     * @param resultSet
     *         待映射的结果集对象。
     * @param resultSetMetaData
     *         结果集的元数据。
     * @return 映射的结果。
     * @throws SQLException
     *         从结果集中获取数据时出现错误。
     */
    @SuppressWarnings("unused")
    private static Object[] mapRow(
            ResultSet resultSet,
            ResultSetMetaData resultSetMetaData
    )
            throws SQLException {
        notNull(resultSet, "rs");
        notNull(resultSetMetaData, "resultSetMetaData");

        int colCount = resultSetMetaData.getColumnCount();
        Object[] row = new Object[colCount];
        for (int i = 1; i <= colCount; ++i) {
            int colType = resultSetMetaData.getColumnType(i);
            Object value;
            switch (colType) {
                default:
                case CHAR:
                case VARCHAR:
                    value = resultSet.getString(i);
                    break;
                case BIT:
                case BOOLEAN:
                case TINYINT:
                case INTEGER:
                case BIGINT:
                    value = resultSet.getLong(i);
                    break;
                case DECIMAL:
                    value = resultSet.getBigDecimal(i);
                    break;
                case REAL:
                case FLOAT:
                case DOUBLE:
                    value = resultSet.getDouble(i);
                    break;
                case DATE:
                case TIMESTAMP:
                    Calendar c = Calendar.getInstance();
                    c.setTime(new java.util.Date(resultSet.getTimestamp(i).getTime()));
                    value = c;
                    break;
                case CLOB:
                case VARBINARY:
                case LONGVARBINARY:
                case BLOB:
                    value = resultSet.getBytes(i);
                    break;
            }

            row[i - 1] = resultSet.wasNull() ? null : value;
        }

        return row;
    }

    private static String clobToString(
            Clob clob
    ) {
        if (clob == null) {
            return null;
        }

        try {
            return readString(clob.getCharacterStream());
        } catch (SQLException ex) {
            throw new DbException(ex);
        } catch (java.io.IOException ex) {
            throw new DbException(ex);
        } finally {
            try {
                clob.free();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * 获取自动生成的键
     *
     * @param statement
     *         可能自动生成键的语句
     * @param autoKey
     *         自动生成的键名
     * @return 自动生成的键值
     * @throws SQLException
     *         如果获取自动生成键时出现SQL命令错误。
     */
    private Object getGeneratedKey(
            Statement statement,
            String autoKey
    )
            throws SQLException {
        Object ret = null;

        if (autoKey != null && !autoKey.isEmpty()) {
            try (final ResultSet resultSet = statement.getGeneratedKeys()) {
                if (resultSet != null) {
                    while (resultSet.next()) {
                        ret = resultSet.getObject(1);
                    }
                }
            }

            if (ret != null) {
                logger.trace("auto generated key \"{}\": \"{}\" found", autoKey, ret);
            } else {
                logger.warn("auto generated key \"{}\" not found", autoKey);
            }
        }

        return ret;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void initialize()
            throws Exception {
        super.initialize();
        this.connection = this.transaction.enroll(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void dispose()
            throws Exception {
        super.dispose();
        this.transaction.revoke(this);
    }

    /**
     * 为SQL语句对象创建SQL参数。
     *
     * @param statement
     *         SQL语句对象。
     * @param parameters
     *         待创建的参数列表。
     * @throws IllegalArgumentException
     *         如果参数{@code statement}是{@code null}。
     * @throws SQLException
     *         如果创建SQL参数出错。
     */
    private void addParameters(
            PreparedStatement statement,
            List<? extends Parameter> parameters
    )
            throws SQLException {
        notNull(statement, "statement");

        if (parameters != null && parameters.size() > 0) {
            for (int i = 0; i < parameters.size(); ++i) {
                final Parameter parameter = parameters.get(i);
                if (parameter != null) {
                    addParameter(statement, i + 1, parameter);
                }
            }
        }
    }

    /**
     * 创建一个准备执行查询的{@link PreparedStatement}对象，并且该对象创建参数。
     *
     * @param sql
     *         SQL语句内容。
     * @param parameters
     *         执行SQL语句的参数，如果此参数的值是{@code null}或者不包含任何元素则不为SQL语句对象创建任何参数。
     * @return 已创建的SQL语句对象。
     * @throws SQLException
     *         如果创建SQL语句对象出错，或者为SQL语句创建参数出错。
     */
    private PreparedStatement prepareStatement(
            String sql,
            List<? extends Parameter> parameters
    )
            throws SQLException {
        final PreparedStatement statement;
        if (isBlank(this.autoKey)) {
            statement = this.connection.execute(conn -> conn.prepareStatement(sql, Statement.NO_GENERATED_KEYS));
        } else if ("*".equals(autoKey)) {
            statement = this.connection.execute(conn -> conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS));
        } else {
            statement = this.connection.execute(conn -> conn.prepareStatement(sql, new String[]{this.autoKey}));
        }

        // TODO: 配置查询超时时间。
        statement.setQueryTimeout(20);

        addParameters(statement, parameters);

        return statement;
    }

    private CallableStatement prepareCall(
            String sql,
            List<? extends Parameter> parameters
    )
            throws SQLException {
        final CallableStatement statement;
        statement = this.connection.execute(connection -> connection.prepareCall(sql));

        // TODO: 配置查询超时时间。
        statement.setQueryTimeout(20);

        addParameters(statement, parameters);

        return statement;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ParameterBuilder newParameter(String name) {
        return new SimpleParameterBuilder(this, name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addParameter(Parameter parameter) {
        this.parameters.add(notNull(parameter, "parameter"));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Session clearParameters() {
        this.parameters.clear();
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Session setStartRowIndex(
            int startRowIndex
    ) {
        this.startRowIndex = startRowIndex;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Session setMaximumRows(
            int maximumRows
    ) {
        this.maximumRows = maximumRows;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Session setFetchSize(
            int fetchSize
    ) {
        greaterThanOrEqual(fetchSize, "fetchSize", 0);

        this.fetchSize = fetchSize;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Session setAutoKey(
            String autoKey
    ) {
        this.autoKey = trimToEmpty(autoKey);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T> List<T> query(
            String sql,
            Mapper<T> mapper
    ) {
        notBlank(sql, "sql");
        notNull(mapper, "mapper");

        final ImmutablePair<String, List<Parameter>> pair = prepareSQL(sql);
        logSql(pair.left, pair.right);

        try (final PreparedStatement statement = prepareStatement(pair.left, pair.right)) {
            statement.setFetchDirection(ResultSet.FETCH_FORWARD);
            if (this.fetchSize > 0) {
                statement.setFetchSize(this.fetchSize);
            }

            return map(statement.executeQuery(), mapper);
        } catch (SQLException ex) {
            throw new DbException(ex);
        } finally {
            reset();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T> Optional<T> querySingle(
            String sql,
            Mapper<T> mapper
    ) {
        notBlank(sql, "sql");
        notNull(mapper, "mapper");

        final ImmutablePair<String, List<Parameter>> pair = prepareSQL(sql);
        logSql(pair.left, pair.right);

        try (final PreparedStatement statement = prepareStatement(pair.left, pair.right)) {
            statement.setFetchDirection(ResultSet.FETCH_FORWARD);
            statement.setFetchSize(1);

            return mapSingle(statement.executeQuery(), mapper);
        } catch (SQLException ex) {
            throw new DbException(ex);
        } finally {
            reset();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int update(
            String sql
    ) {
        notBlank(sql, "sql");

        final ImmutablePair<String, List<Parameter>> pair = prepareSQL(sql);
        logSql(pair.left, pair.right);

        try (final PreparedStatement statement = prepareStatement(pair.left, pair.right)) {
            int ret = statement.executeUpdate();

            this.transaction.setDirty();

            this.lastRowId = this.autoKey.isEmpty() ? null : getGeneratedKey(statement, autoKey);

            return ret;
        } catch (SQLException ex) {
            throw new DbException(ex);
        } finally {
            this.parameters.clear();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T> List<T> queryProc(
            String procedure,
            Mapper<T> mapper
    ) {
        notBlank(procedure, "procedure");
        notNull(mapper, "mapper");

        final ImmutablePair<String, List<Parameter>> pair = prepareProc(procedure, true);
        logSql(pair.left, pair.right);

        try (final CallableStatement statement = prepareCall(pair.left, pair.right)) {
            statement.execute();

            // 存储过程的操作结果无法判断，所以假定产生了脏数据。
            this.transaction.setDirty();

            retrieveOutParameters(statement, pair.getRight());

            return map(getProcResultSet(statement), mapper);
        } catch (SQLException ex) {
            throw new DbException(ex);
        } finally {
            this.parameters.clear();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T> Optional<T> querySingleProc(
            String procedure,
            Mapper<T> mapper
    ) {
        notBlank(procedure, "procedure");
        notNull(mapper, "mapper");

        final ImmutablePair<String, List<Parameter>> pair = prepareProc(procedure, true);
        logSql(pair.left, pair.right);

        try (final CallableStatement statement = prepareCall(pair.left, pair.right)) {
            statement.execute();

            // 存储过程中可能包含多次操作，所以不论返回类型都需要提交。
            this.transaction.setDirty();

            retrieveOutParameters(statement, pair.getRight());

            return mapSingle(getProcResultSet(statement), mapper);
        } catch (SQLException ex) {
            throw new DbException(ex);
        } finally {
            reset();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int updateProc(
            String procedure
    ) {
        notBlank(procedure, "procedure");

        final ImmutablePair<String, List<Parameter>> pair = prepareProc(procedure, false);
        logSql(pair.left, pair.right);

        try (final CallableStatement statement = prepareCall(pair.left, pair.right)) {
            statement.execute();

            this.transaction.setDirty();

            retrieveOutParameters(statement, pair.getRight());

            return statement.getUpdateCount();
        } catch (SQLException ex) {
            throw new DbException(ex);
        } finally {
            this.parameters.clear();
        }
    }

    /**
     * {@inheritDoc}
     */
    public final Object getLastRowId() {
        return this.lastRowId;
    }

    /**
     * 获取存储过程返回的结果集。
     *
     * @param statement
     *         执行存储过程的Statement对象。
     * @return 返回的结果集。
     * @throws SQLException
     *         如果获取结果集时出现错误，或者能够获得执行结果，但是此结果不是结果集类型。
     */
    protected ResultSet getProcResultSet(
            CallableStatement statement
    )
            throws SQLException {
        ResultSet resultSet = statement.getResultSet();
        if (resultSet == null) {
            resultSet = (ResultSet) statement.getObject(1);
        }

        return resultSet;
    }

    /**
     * 判断是否需要继续遍历结果集，获取更多的行。
     *
     * @param rows
     *         当前已遍历的结果集行数。
     * @return 是否需要继续遍历。
     */
    private boolean needMoreData(
            int rows
    ) {
        return rows < this.maximumRows || this.maximumRows == 0;
    }

    /**
     * 将结果集映射为{@code java.util.List}对象。
     *
     * @param resultSet
     *         结果集。
     * @param mapper
     *         映射器。
     * @return 映射结果。如果参数{@code rs}是{@code null}或者不包含任何行则返回{@code Collection.emptyList()}。
     * @throws SQLException
     *         如果获取结果集的元数据出现错误或者从结果集中读取数据失败。
     * @throws NullPointerException
     *         如果参数{@code mapper}是{@code null}。
     */
    private <T> List<T> map(
            ResultSet resultSet,
            Mapper<T> mapper
    )
            throws SQLException {
        notNull(mapper, "mapper");

        if (resultSet != null) {
            final ResultSetMetaData resultSetMetaData = resultSet.getMetaData();

            final List<T> resultList = new ArrayList<>();

            int i = 0;

            final DbRow row = new InnerDbRow(resultSet, resultSetMetaData);

            while (row.hasData() && i < this.startRowIndex) {
                if (mapper.canGetNewResult(row)) {
                    ++i;
                }
                row.next();
            }

            i = 0;

            while (row.hasData() && needMoreData(i)) {
                if (mapper.canGetNewResult(row)) {
                    resultList.add(mapper.execute(row));
                    ++i;
                } else {
                    mapper.execute(row);
                }

                row.next();
            }

            if (mapper.canGetNewResult(row) && needMoreData(i)) {
                resultList.add(mapper.execute(row));
            }

            return resultList;
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * 将结果集的第一行第一列映射为{@code java.util.Optional}对象。
     *
     * @param resultSet
     *         结果集。
     * @param mapper
     *         映射器。
     * @return 映射结果。如果参数{@code resultSet}是{@code null}或者不包含任何行则返回{@code Optional.empty()}。
     * @throws SQLException
     *         如果获取结果集的元数据出现错误或者从结果集中读取数据失败。
     * @throws NullPointerException
     *         如果参数{@code mapper}是{@code null}。
     */
    private <T> Optional<T> mapSingle(
            ResultSet resultSet,
            Mapper<T> mapper
    )
            throws SQLException {
        notNull(mapper, "mapper");

        if (resultSet != null) {
            final ResultSetMetaData resultSetMetaData = resultSet.getMetaData();

            final DbRow row = new InnerDbRow(resultSet, resultSetMetaData);

            while (row.hasData()) {
                if (mapper.canGetNewResult(row)) {
                    return Optional.ofNullable(mapper.execute(row));
                } else {
                    mapper.execute(row);
                }

                row.next();
            }

            if (mapper.canGetNewResult(row)) {
                return Optional.ofNullable(mapper.execute(row));
            } else {
                return Optional.empty();
            }
        } else {
            return Optional.empty();
        }
    }

    /**
     * 将所有的执行状态复位。
     */
    private void reset() {
        this.startRowIndex = 0;
        this.maximumRows = 0;
        this.parameters.clear();
    }

    /**
     * 从原始SQL命令中找到所有的命名参数并替换，构造出实际执行的SQL命令
     * <p>
     * <p>
     * 替换规则：
     * </p>
     * <ul>
     * <li>形如{@code #name#}的字符串被替换为{@code ?}，并在参数列表中加入该参数。</li>
     * <li>形如{@code $name$}的字符串被替换为参数的实际内容，参数列表不变。</li>
     * </ul>
     *
     * @param sql
     *         待处理的原始SQL命令。
     * @return 实际执行的SQL命令和参数列表。
     * @throws IllegalArgumentException
     *         如果参数{@code sql}是{@code null}或者只包含空白字符。
     */
    private ImmutablePair<String, List<Parameter>> prepareSQL(
            String sql
    ) {
        notBlank(sql, "sql");

        final StringBuilder sqlBuilder = new StringBuilder();
        final List<Parameter> pl = new ArrayList<>();

        prepareSQL(sql, sqlBuilder, pl);

        return ImmutablePair.of(sqlBuilder.toString(), pl);
    }

    /**
     * 根据存储过程名和参数集合构造SQL语句。
     *
     * @param procedure
     *         存储过程名。
     * @param hasResult
     *         此存储过程是否可以返回结果集。
     * @return 实际执行的SQL命令和参数列表。
     * @throws IllegalArgumentException
     *         如果参数{@code proc}是{@code null}或者只包含空白字符。
     */
    private ImmutablePair<String, List<Parameter>> prepareProc(
            String procedure,
            boolean hasResult
    ) {
        notBlank(procedure, "procedure");

        final StringBuilder sqlBuilder = new StringBuilder();
        final List<Parameter> plist = new ArrayList<>();

        prepareProc(procedure, sqlBuilder, hasResult, plist);

        return ImmutablePair.of(sqlBuilder.toString(), plist);
    }

    protected void prepareSQL(
            String sql,
            StringBuilder sqlBuilder,
            List<? super Parameter> pl
    ) {
        final Matcher matcher = PARAMETER_PLACEHOLDER.matcher(sql);
        int pos = 0;

        while (matcher.find()) {
            if (pos < matcher.start()) {
                sqlBuilder.append(sql, pos, matcher.start());
            }

            pos = matcher.end();

            String g1 = matcher.group(2);
            String g2 = matcher.group(4);

            if (g1 != null && !g1.isEmpty()) {
                sqlBuilder.append("?");

                // Find parameter by name.
                final Parameter parameter = getParameterByName(g1, this.parameters);
                if (parameter != null) {
                    pl.add(parameter);
                } else {
                    throw new DbException("unknown parameter \"" + g1 + "\"");
                }
            } else if (g2 != null && !g2.isEmpty()) {
                // Find parameter by name.
                final Parameter parameter = getParameterByName(g2, this.parameters);
                if (parameter != null) {
                    sqlBuilder.append(parameter.getValue());
                } else {
                    throw new DbException("unknown parameter \"" + g2 + "\"");
                }
            } else {
                assert false : String.format("matcher: \"%s\" <--> \"%s\".", sql, matcher.group());
            }
        }
        if (pos < sql.length()) {
            sqlBuilder.append(sql.substring(pos));
        }
    }

    protected final List<Parameter> getParameters() {
        return this.parameters;
    }

    protected void prepareProc(
            String procedure,
            StringBuilder sqlBuilder,
            boolean hasResult,
            List<? super Parameter> pl
    ) {
        sqlBuilder.append("{call ");
        sqlBuilder.append(procedure.toUpperCase());
        sqlBuilder.append("(");

        pl.addAll(getParameters());

        sqlBuilder.append(repeat("?", ", ", pl.size()));
        sqlBuilder.append(")}");

    }

    /**
     * 记录待执行的SQL和参数到日志。
     *
     * @param sql
     *         待执行的SQL。
     * @param parameters
     *         参数。
     * @throws NullPointerException
     *         如果参数{@code parameters}是{@code null}。
     */
    private void logSql(
            String sql,
            List<? extends Parameter> parameters
    ) {
        notNull(parameters, "parameters");

        logger.debug("execute sql: {}", new Object() {
            @Override
            public String toString() {
                final StringBuilder s = new StringBuilder();

                s.append("\n").append("text: ").append(sql);

                for (int i = 0; i < parameters.size(); ++i) {
                    s.append("\n").append("parameter: ").append(i + 1).append(parameters.get(i));
                }

                return s.toString();
            }
        });
    }

    /**
     * 为指定的{@code java.sql.PrepareStatement}对象中创建SQL参数。
     *
     * @param statement
     *         待添加参数的{@code java.sql.PrepareStatement}对象。
     * @param index
     *         参数下标，从{@code 1}开始。
     * @param parameter
     *         待添加的参数。
     * @throws IllegalArgumentException
     *         如果参数{@code stm}或者{@code parameter}是{@code null}或者参数{@code index}小于{@code 1}或者参数{@code parameter}的参数类型无法识别。
     * @throws SQLException
     *         如果创建SQL参数出错。
     */
    protected void addParameter(
            PreparedStatement statement,
            int index,
            Parameter parameter
    )
            throws SQLException {
        notNull(statement, "statement");
        greaterThanOrEqual(index, "index", 1);
        notNull(parameter, "parameter");

        // 参数值应当已经被正规化。
        final Object value = parameter.getValue();
        final ParameterType pType = parameter.getParameterType();
        final int sqlType = toSqlType(pType);
        final Set<ParameterDirection> pDir = parameter.getDirections();

        if (pDir.contains(ParameterDirection.OUT)) {
            if (statement instanceof CallableStatement) {
                ((CallableStatement) statement).registerOutParameter(index, sqlType);
                if (!pDir.contains(ParameterDirection.IN)) {
                    // 该参数只被指定为传出参数，那么不需要设置值。
                    return;
                }
            } else {
                logger.warn("statement does not support out parameter[{}]", index);
                return;
            }
        }

        if (value == null) {
            statement.setNull(index, sqlType);
        } else {
            switch (pType) {
                case STRING:
                    statement.setString(index, value.toString());
                    break;
                case INTEGER:
                    statement.setLong(index, ((Number) value).longValue());
                    break;
                case DECIMAL:
                    statement.setBigDecimal(index, (BigDecimal) value);
                    break;
                case DATETIME:
                case START_DATE:
                case START_MONTH:
                case START_YEAR:
                case END_DATE:
                case END_MONTH:
                case END_YEAR:
                    statement.setTimestamp(index, new java.sql.Timestamp(((Date) value).getTime()));
                    break;
                case DOUBLE:
                    statement.setDouble(index, ((Number) value).doubleValue());
                    break;
                case BOOLEAN:
                    statement.setBigDecimal(index, ((Boolean) value) ? BigDecimal.ONE : BigDecimal.ZERO);
                    break;
                case BINARY:
                    statement.setBlob(index, new ByteArrayInputStream((byte[]) parameter.getValue()));
                    break;
                case RESULT_SET:
                    // 此类型的参数仅用于传出参数。
                    break;
                default:
                    throw new IllegalArgumentException("illegal parameter type: " + pType);
            }
        }
    }

    /**
     * 根据参数类型获取对应的SQL参数类型。
     *
     * @param pType
     *         参数类型。
     * @return 对应的SQL参数类型。
     * @throws IllegalArgumentException
     *         如果参数{@code pType}不是可以识别的参数类型。
     */
    int toSqlType(
            ParameterType pType
    ) {
        switch (pType) {
            case STRING:
                return VARCHAR;
            case INTEGER:
                return BIGINT;
            case DECIMAL:
                return DECIMAL;
            case DATETIME:
            case START_DATE:
            case START_MONTH:
            case START_YEAR:
            case END_DATE:
            case END_MONTH:
            case END_YEAR:
                return TIMESTAMP;
            case DOUBLE:
                return DOUBLE;
            case BOOLEAN:
                return BIGINT;
            case BINARY:
                return BLOB;
            case RESULT_SET:
                return REF_CURSOR;
            default:
                throw new IllegalArgumentException("invalid argument \"pType\": " + pType);
        }
    }

    /**
     * 接口{@link DbRow}的一个内部实现。
     *
     * @author Haart
     */
    private static final class InnerDbRow
            implements DbRow {
        private final ResultSet rs;
        private final ResultSetMetaData resultSetMetaData;
        private final int columnCount;
        private boolean lastNextResult;

        /**
         * 初始化InnerDbRow的实例。
         *
         * @param resultSet
         *         结果集对象。
         * @param resultSetMetaData
         *         结果集相关的元数据。
         * @throws IllegalArgumentException
         *         如果参数{@code resultSet}或者{@code resultSetMetaData}是{@code null}。
         * @throws DbException
         *         如果通过结果集对象获取下一个记录时出错，或者通过结果集对象元数据判断结果列数时出错。
         */
        InnerDbRow(
                ResultSet resultSet,
                ResultSetMetaData resultSetMetaData
        ) {
            notNull(resultSet, "resultSet");
            notNull(resultSetMetaData, "resultSetMetaData");

            this.rs = resultSet;
            this.resultSetMetaData = resultSetMetaData;
            try {
                this.lastNextResult = resultSet.next();
                this.columnCount = this.resultSetMetaData.getColumnCount();
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getString(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return null;
            }

            try {
                if (this.resultSetMetaData.getColumnType(index + 1) == java.sql.Types.CLOB) {
                    final Clob clob = rs.getClob(index + 1);
                    return rs.wasNull() ? null : clobToString(clob);
                } else {
                    final String str = rs.getString(index + 1);
                    return rs.wasNull() ? null : str;
                }
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Integer getInteger(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return 0;
            }

            try {
                final int i = rs.getInt(index + 1);
                return !rs.wasNull() ? i : null;
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Long getLong(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return null;
            }

            try {
                final long l = rs.getLong(index + 1);
                return !rs.wasNull() ? l : null;
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public BigDecimal getDecimal(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return null;
            }

            try {
                return rs.getBigDecimal(index + 1);
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Double getDouble(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return null;
            }

            try {
                final double d = rs.getDouble(index + 1);
                return !rs.wasNull() ? d : null;
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Date getDate(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return null;
            }

            try {
                final Timestamp timestamp = rs.getTimestamp(index + 1);
                return !rs.wasNull() ? new Date(timestamp.getTime()) : null;
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Boolean getBoolean(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return null;
            }

            try {
                boolean b = rs.getBoolean(index + 1);
                return !rs.wasNull() ? b : null;
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public byte[] getBinary(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return null;
            }

            try {
                return rs.getBytes(index + 1);
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Object getObject(
                int index
        ) {
            validIndex(index, "index");

            if (index >= this.columnCount) {
                return null;
            }

            try {
                return rs.getObject(index + 1);
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int length() {
            return this.columnCount;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean hasData() {
            return this.lastNextResult;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void next() {
            try {
                this.lastNextResult = this.rs.next();
            } catch (SQLException ex) {
                throw new DbException(ex);
            }
        }
    }

}
